package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * redis缓存操作工具类
 * 只针对String类型的数据
 * @author LENOVO
 * @Date 2022-11-28
 */
@Component
public class StringRedisClient {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     *添加缓存  使用ttl过期
     ** @param keyPrefix key前缀
     ** @param id 数据库id
     ** @param date 数据库查询到的数据
     ** @param time 过期时间
     ** @param unit 过期时间单位
     ** @param <ID> id类型
     ** @param <T> 数据类型
     **/
    public<ID,T> void set(String keyPrefix ,ID id,T date,Long time,TimeUnit unit) {
        // 拼接生成key
        String key=keyPrefix+id.toString();
        // 序列化
        String dateStr = JSONUtil.toJsonStr(date);
        // 保存缓存 设置 ttl过期时间
        stringRedisTemplate.opsForValue().set(key,dateStr,time,unit);
    }

    /**
     * 添加缓存 使用逻辑过期
     * @param keyPrefix key前缀
     * @param id 数据库id
     * @param date 数据库查询到的数据
     * @param time 过期时间
     * @param unit 过期时间单位
     * @param <ID> id类型
     * @param <T> 数据类型
     */
    public<ID,T> void setWithLogicalExpire(String keyPrefix,ID id,T date,Long time,TimeUnit unit) {
        // 拼接生成key
        String key=keyPrefix+id.toString();
        // 创建组合对象
        RedisData<T> redisData = new RedisData<>();
        // 设置过期时间戳
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 设置数据
        redisData.setData(date);
        // 反序列化
        String jsonStr = JSONUtil.toJsonStr(redisData);
        // 添加缓存
        stringRedisTemplate.opsForValue().set(key, jsonStr);

    }

    /**
     *查询 设置 ttl过期缓存的数据 解决缓存穿透
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID,R> query){
        String key=keyPrefix+id.toString();
        //先查缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 存在直接返回
        if(StrUtil.isNotBlank(json)){
            // 反序列化
           return JSONUtil.toBean(json, type);
        }

        // 是否为空指的是“” 并不是null ""为数据库不存在存的空值
        // null为缓存不存在，数据库存不存在并不清楚
        if(json!=null){
            return null;
        }

        // 查数据库
        R date = query.apply(id);
        if(date==null) {
            // 数据库不存在 缓存设置空值 ""
            set(keyPrefix, id, "", 60L, TimeUnit.SECONDS);
            // 返回空
            return null;
        }
        // 写入redis
        set(keyPrefix, id,date, time, unit);
        return date;
    }

    /**
     * 查询 设置 逻辑过期的缓存 解决缓存击穿（热点key）
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix,String lockKeyPrefix,ID id,Class<R> type,Function<ID,R> query,Long time, TimeUnit unit){
        String key=keyPrefix+id.toString();
        String lockKey=lockKeyPrefix+id.toString();
        //先查缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(json)){
            //未命中 为空 直接返回  不是热点key
            return null;
        }
        // 命中
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), type);
        // 是否过期
        if(redisData.getExpireTime().isAfter(LocalDateTime.now())){
            //未过期 直接返回
            return r;
        }
        // 过期 获取锁
        boolean b = tryLock(lockKey);
        if(b){
            try {
                // 获取锁成功 开启另一个线程 重建缓存
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    //查数据库
                    R r1 = query.apply(id);

                    if(r1!=null){
                        // 数据库不为空 写缓存
                        setWithLogicalExpire(keyPrefix, id, r1, time,unit);
                    }
                });

            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 释放锁
                unlock(lockKey);
            }
        }
        // 返回数据
        return r;
    }

    /**
     * 查询 设置 互斥锁的缓存  解决缓存击穿（热点key）
     * @param <R>
     * @param <ID>
     * @return
     */
    public <R,ID> R queryWithMutex(String keyPrefix,String lockKeyPrefix,ID id,Class<R> type,Function<ID,R> query,Long time, TimeUnit unit){
        String key=keyPrefix+id.toString();
        String lockKey=lockKeyPrefix+id.toString();
        // 先查缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(json)|| json!=null){
            // 存在直接返回
            return JSONUtil.toBean(json, type);
        }
        //不存在 先获取锁 在查询数据库
        boolean tryLock = tryLock(lockKey);
        try {
            if(!tryLock){
                // 获取锁失败 睡眠重试
                Thread.sleep(10);
               return queryWithMutex( keyPrefix,lockKeyPrefix,id, type,query,time,unit);
            }
            // 获取锁成功 查数据库
            R r = query.apply(id);
            if(r==null){
                // 为空 写缓存 空值 “”
                stringRedisTemplate.opsForValue().set(key, "", time,unit);
                return null;
            }
            // 不会空 写缓存
            set(keyPrefix, id, r, 3L, TimeUnit.MINUTES);
            return r;
        } catch (InterruptedException e) {
           throw new RuntimeException(e);
        } finally {
            //释放锁
            unlock(lockKey);
        }

    }

    /**
     * 获取锁
     * @return
     */
    private boolean tryLock(String lockKey) {
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(1),RedisConstants.SHOP_LOCK_TTL,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }
//
    /**
     * 释放锁
     */
    private void unlock(String lockKey) {
        stringRedisTemplate.delete(lockKey);
    }



}
